Le carnet On est
Annale 2022-2023 · Session Normale 2 · ING2-GSI/GIA

Examen Réseau — Session Normale 2

2 heures, 4 exercices : questions de cours (handler, dup/dup2, blocage clavier, processus vs thread, sémaphores), cabines d'essayage avec signaux SIGUSR1/2, jeu plus-petit/plus-grand par IPC, et sockets TCP/UDP client-serveur.

2h · ING2 GSI/GIA 4 exercices · 20 points 2 feuilles recto-verso autorisées

i. Le sujet

Le PDF ne s'affiche pas. Télécharger le sujet.

↗ Sujet en plein écran

ii. Notions mobilisées

ExoSujetNotion cléChapitre
1Courssignal, dup/dup2, getchar/termios, process vs thread, sémaphoresCours 1 · Cours 2 · Cours 3 · Cours 4
2Cabines + SIGUSR1/2sigaction, kill, état partagéCours 2
3Jeu plus-petit/grandFiles de messages, mémoire partagée, sémaphoresCours 6 · Cours 2
4.1TCP client/serveursocket, bind, listen, accept, connectCours 5
4.2UDP client/serveursendto, recvfrom, sans connexionCours 5

1. Exercice 1 — Questions de cours (5 pts)

Q1 — Ignorer un signal via le handler

Deux approches selon le degré de souplesse souhaité :

Solution rapide — utiliser la constante SIG_IGN à la place d'un handler :

signal(SIGUSR1, SIG_IGN);                // ignore SIGUSR1
// ou plus portable :
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGUSR1, &sa, NULL);

Solution explicite — un handler qui ne fait rien :

void handler(int sig) { /* rien */ }
signal(SIGUSR1, handler);

Attention : SIGKILL et SIGSTOP ne peuvent pas être ignorés ni interceptés.

Q2 — Utilité et différence de dup / dup2

Les deux primitives dupliquent un descripteur de fichier dans la table des descripteurs du processus. Cas d'usage typique : rediriger stdin ou stdout vers un fichier ou un tube (cf. Cours 3, pipeline shell).

int dup(int fd) Alloue le plus petit descripteur libre et le fait pointer sur le même objet que fd. Renvoie le nouveau descripteur.
int dup2(int oldfd, int newfd) Force le numéro exact newfd. Si newfd était ouvert, il est d'abord fermé silencieusement. C'est la version idéale pour rediriger un descripteur précis (0, 1, 2).

Exemple typique pour rediriger stdout vers un fichier :

int fd = open("sortie.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, 1);          // stdout pointe maintenant sur sortie.txt
close(fd);
printf("écrit dans le fichier !\n");
Q3 — Bloquer l'accès au clavier (illustrer en C)

« Bloquer le clavier » signifie en pratique désactiver l'écho et passer en mode non-canonique via termios, afin que les caractères tapés ne soient ni affichés ni transmis ligne par ligne.

#include <stdio.h>
#include <termios.h>
#include <unistd.h>

int main(void) {
   struct termios old, new_t;
   tcgetattr(STDIN_FILENO, &old);          // sauvegarde de la conf
   new_t = old;
   new_t.c_lflag &= ~(ICANON | ECHO);      // désactive canon. + écho
   tcsetattr(STDIN_FILENO, TCSANOW, &new_t);

   printf("Clavier bloqué pendant 5 secondes...\n");
   sleep(5);

   tcsetattr(STDIN_FILENO, TCSANOW, &old); // restauration
   return 0;
}

Variante plus radicale : fermer stdin avec close(0), ou bloquer le signal SIGINT pour empêcher Ctrl+C de terminer le programme.

Q4 — Différence processus vs thread
CritèreProcessusThread
Espace d'adressageIndépendantPartagé dans le processus parent
Pile d'exécutionUne par processusUne par thread
CréationLourde (fork copie pages)Légère (pthread_create)
CommunicationIPC (pipes, signaux, msg…)Mémoire partagée directe
SynchronisationGérée par OSManuelle (mutex, sémaphores)
Crash isolé ?Oui, indépendantNon — un crash tue le processus entier
Q5 — Utilité des sémaphores

Un sémaphore est un compteur protégé sur lequel deux opérations atomiques sont définies : P (wait, décrémente, bloque si négatif) et V (post, incrémente, débloque un attendant).

Trois usages classiques :

  • Exclusion mutuelle (mutex binaire = sémaphore à valeur 0 ou 1) — un seul processus dans la section critique.
  • Gestion d'un pool de ressources (sémaphore à valeur n) — au plus n consommateurs simultanés (parking à n places, n connexions DB…).
  • Synchronisation / rendez-vous — forcer un processus à attendre qu'un autre ait fini une étape (sémaphore initialisé à 0, V par le producteur, P par le consommateur).

2. Exercice 2 — Cabines d'essayage et signaux (6 pts)

Trois processus communiquent par signaux : Gestion (maintient le compteur de cabines libres), Entrée (demande une cabine, affiche la réponse), Sortie (signale la libération).

Protocole de signaux
  • Entrée → Gestion : SIGUSR1 pour demander une entrée.
  • Gestion → Entrée : SIGUSR2 si une cabine est libre (autorisée), SIGUSR1 sinon (interdite).
  • Sortie → Gestion : SIGUSR2 à chaque sortie pour incrémenter le compteur.

Q1 — Handler SIGUSR1 dans Gestion

Quand Gestion reçoit SIGUSR1, c'est une demande d'entrée. Il consulte nbCabinesDisponibles et répond au processus Entrée (dont le PID est connu via une variable globale).

extern int nbCabinesDisponibles;       // initialisé au démarrage
extern pid_t pidEntree;                // récupéré au démarrage

void handlerSIGUSR1_Gestion(int sig) {
   if (nbCabinesDisponibles > 0) {
      nbCabinesDisponibles--;
      printf("Gestion : entrée autorisée (cabines restantes : %d)\n", nbCabinesDisponibles);
      kill(pidEntree, SIGUSR2);          // répondre OUI
   } else {
      printf("Gestion : entrée interdite (aucune cabine libre)\n");
      kill(pidEntree, SIGUSR1);          // répondre NON
   }
}
⚠ Réentrance et asynchronisme Un handler de signal doit utiliser uniquement des fonctions async-signal-safe. printf n'en fait pas partie au sens strict (POSIX) — en code de production, préférer write. Dans le contexte de l'examen, printf est toléré pour la lisibilité.

Q2 — Handler unique SIGUSR1/SIGUSR2 dans Entrée

Le processus Entrée reçoit soit SIGUSR1 (interdit), soit SIGUSR2 (autorisé), et affiche le message correspondant.

void handlerEntree(int sig) {
   if (sig == SIGUSR2)
      printf("Entrée : entrée autorisée\n");
   else if (sig == SIGUSR1)
      printf("Entrée : entrée interdite\n");
}

// installation : un seul handler pour les deux signaux
int main(void) {
   struct sigaction sa;
   sa.sa_handler = handlerEntree;
   sigemptyset(&sa.sa_mask);
   sa.sa_flags = 0;
   sigaction(SIGUSR1, &sa, NULL);
   sigaction(SIGUSR2, &sa, NULL);

   while (1) {
      // envoyer SIGUSR1 à Gestion pour demander une entrée…
      pause();                          // attendre la réponse
   }
}

Q3 — Alternatives aux signaux

Les signaux ne transportent pas de données utiles (juste l'identité du signal). Pour un protocole plus riche, on utiliserait des IPC :

Files de messages (msgsnd/msgrcv) Chaque message porte un type et un payload. Idéal pour le protocole demande/réponse car le type peut servir de canal d'adressage.
Mémoire partagée (shmget/shmat) + sémaphores La variable nbCabinesDisponibles est dans le segment partagé. Les sémaphores synchronisent les accès. Très rapide mais demande beaucoup de discipline (race conditions).
  • Tubes anonymes entre processus apparentés (cf. Cours 3) — communication octet par octet.
  • Tubes nommés (FIFO) pour processus indépendants.
  • Sockets locales (AF_UNIX) — protocole bidirectionnel sans réseau.

3. Exercice 3 — Jeu du plus grand/plus petit (3 pts)

Deux processus jouent au jeu du nombre mystère. Choix proposé : files de messages, mémoire partagée ou sémaphores. La file de messages est la solution la plus naturelle.

Choix : files de messages

Une seule file, deux types de messages (1 = du joueur vers l'ordinateur, 2 = de l'ordinateur vers le joueur). Chaque message porte un entier (la proposition ou la réponse encodée).

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <time.h>

struct msg {
   long type;             // 1 = joueur→ordi, 2 = ordi→joueur
   int  val;              // proposition OU 0=trop petit, 1=trop grand, 2=gagné
};

int main(void) {
   int mq = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
   struct msg m;

   if (fork() == 0) {                         // ────── ORDINATEUR ──────
      srand(time(NULL));
      int mystere = rand() % 100 + 1;
      while (1) {
         msgrcv(mq, &m, sizeof(m.val), 1, 0);   // reçoit proposition
         m.type = 2;
         if      (m.val < mystere) m.val = 0;       // trop petit
         else if (m.val > mystere) m.val = 1;       // trop grand
         else { m.val = 2; msgsnd(mq, &m, sizeof(m.val), 0); break; }
         msgsnd(mq, &m, sizeof(m.val), 0);
      }
      exit(0);
   } else {                                  // ────── JOUEUR ──────
      int coups = 0;
      while (1) {
         printf("Devine : ");
         scanf("%d", &m.val);
         m.type = 1;
         msgsnd(mq, &m, sizeof(m.val), 0);
         msgrcv(mq, &m, sizeof(m.val), 2, 0);
         coups++;
         if      (m.val == 0) printf("trop petit\n");
         else if (m.val == 1) printf("trop grand\n");
         else { printf("Gagné en %d coups !\n", coups); break; }
      }
      wait(NULL);
      msgctl(mq, IPC_RMID, NULL);             // nettoyage
   }
   return 0;
}
🔑 Pourquoi les files de messages plutôt que sémaphores ou mémoire partagée ?
Le protocole demande/réponse correspond exactement au modèle des files de messages : chaque message porte une donnée et un type qui sert de filtre. Avec sémaphores + mémoire partagée, il faudrait coder ces conventions à la main (variables drapeaux, blocs critiques). Plus simple et plus expressif avec msgsnd / msgrcv.

4. Exercice 4 — Partie 1 : TCP client/serveur (3 pts)

Q1 — Compléter le client TCP

Les zones marquées /*…*/ dans l'ordre :

// 1. Création de la socket TCP
sock = socket(AF_INET, SOCK_STREAM, 0)

// 2. Famille d'adresse
serveur.sin_family = AF_INET

// 3. Connexion au serveur
connect(sock, (struct sockaddr*)&serveur, sizeof(serveur)) < 0

// 4. Envoi du message
ret = write(sock, buf_write, strlen(buf_write))

// 5. Réception de la réponse
ret = read(sock, buf_read, sizeof(buf_read))

Q2 — Code du serveur TCP

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char** argv) {
   int ecoute, dialogue;
   struct sockaddr_in serveur, client;
   socklen_t len = sizeof(client);
   char buf[256];

   if (argc != 2) { fprintf(stderr, "Usage : %s port\n", argv[0]); exit(1); }

   ecoute = socket(AF_INET, SOCK_STREAM, 0);
   serveur.sin_family      = AF_INET;
   serveur.sin_port        = htons(atoi(argv[1]));
   serveur.sin_addr.s_addr = INADDR_ANY;

   bind(ecoute, (struct sockaddr*)&serveur, sizeof(serveur));
   listen(ecoute, 5);

   while (1) {
      dialogue = accept(ecoute, (struct sockaddr*)&client, &len);
      printf("Nouveau client : %s:%d\n",
              inet_ntoa(client.sin_addr), ntohs(client.sin_port));

      int n;
      while ((n = read(dialogue, buf, 255)) > 0) {
         buf[n] = '\0';
         printf("Reçu : %s\n", buf);
         write(dialogue, buf, n);              // écho
      }
      close(dialogue);
   }
   return 0;
}

Variante multi-clients : fork() après accept (cf. annale Session Normale).

5. Exercice 4 — Partie 2 : UDP client/serveur (3 pts)

Q1 — Compléter les deux programmes

Côté client UDP, dans l'ordre :

// 1. Création de la socket UDP
sock = socket(AF_INET, SOCK_DGRAM, 0)

// 2. Port serveur (le port est passé en argv[3])
serveur.sin_port = htons(sport)

// 3. Réception d'un datagramme
ret = recvfrom(sock, buf_read, sizeof(buf_read), 0,
               (struct sockaddr*)&serveur, &serveur_len)

// 4. Envoi d'un datagramme
ret = sendto(sock, buf_write, strlen(buf_write), 0,
             (struct sockaddr*)&serveur, sizeof(serveur))

Côté serveur UDP :

// 1. Création de la socket
(sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1

// 2. Bind
bind(sock, (struct sockaddr*)&serveur, sizeof(serveur))

// 3. recvfrom
ret = recvfrom(sock, buf_read, sizeof(buf_read), 0,
               (struct sockaddr*)&client, &client_len)

// 4. sendto
ret = sendto(sock, buf_write, strlen(buf_write), 0,
             (struct sockaddr*)&client, client_len)

Q2 — Sortie sur 127.0.0.1:7000 (3 itérations)

Le client envoie 3 messages numérotés (nb_message++). À chaque tour, le serveur reçoit, affiche et répond. Le client reçoit la réponse, affiche puis envoie le suivant.

Trace approximative (avec id = "C", port serveur 7000, sleep(6) après chaque sendto serveur) :

Client  C  (127.0.0.1:???0) reçoit de (127.0.0.1:7000) : ...
Client  C envoie a (127.0.0.1:7000) (127.0.0.1:???0) : #C=000
Serveur S (127.0.0.1:7000) reçu le message #C=000
Client  C  (127.0.0.1:???0) reçoit de (127.0.0.1:7000) : #S reponse000
Client  C envoie a (127.0.0.1:7000) (127.0.0.1:???0) : #C=001
Serveur S (127.0.0.1:7000) reçu le message #C=001
...
J'ai fini, au revoir
Détail subtil du protocole Le client commence par un recvfrom : il attend que le serveur lui parle en premier. Or le serveur attend lui aussi en recvfrom. Sans message initial du client, les deux se bloquent éternellement. C'est un bug de l'énoncé : il faudrait que le client envoie en premier (sendto avant recvfrom). À mentionner en commentaire.

Q3 — Fermer la connexion sur « FIN »

while (1) {
   printf("Message : ");
   scanf("%s", buf_write);
   if (strcmp(buf_write, "FIN") == 0) {
      sendto(sock, "FIN", 3, 0,
             (struct sockaddr*)&serveur, sizeof(serveur));
      break;                 // quitter la boucle
   }
   sendto(sock, buf_write, strlen(buf_write), 0,
          (struct sockaddr*)&serveur, sizeof(serveur));
   recvfrom(sock, buf_read, sizeof(buf_read), 0,
            (struct sockaddr*)&serveur, &serveur_len);
   printf("Réponse : %s\n", buf_read);
}
close(sock);

Côté serveur, on peut tester if (strcmp(buf_read, "FIN") == 0) break; pour quitter aussi.

UDP vs TCP — récap
  • TCP (SOCK_STREAM) — orienté connexion, fiable, ordonné. Demande connect/accept/listen. Pas de notion de message : c'est un flux d'octets.
  • UDP (SOCK_DGRAM) — sans connexion, non fiable, non ordonné. sendto/recvfrom. Chaque datagramme est indépendant.

Notions ré-activées

  • SIG_IGN ignore un signal (sauf SIGKILL/SIGSTOP)
  • dup(fd) = plus petit libre · dup2(old, new) = numéro précis
  • termios + ICANON/ECHO pour désactiver l'écho clavier
  • Sémaphore = compteur protégé (exclusion, ressources, sync)
  • Signaux user = SIGUSR1, SIGUSR2 (libres pour l'utilisateur)
  • Handler unique pour 2 signaux : dispatch via le paramètre sig
  • Files de messages = type + payload, parfait pour demande/réponse
  • TCP = socket(SOCK_STREAM) + connect/listen/accept + read/write
  • UDP = socket(SOCK_DGRAM) + bind + sendto/recvfrom
  • htons/ntohs sur les ports, htonl/ntohl sur les IP
  • inet_aton/inet_ntoa conversion texte/binaire IPv4